﻿using System;
using System.Collections.Generic;
using System.Linq;
using JetBlack.TopicBus.Adapters;

namespace JetBlack.TopicBus.ExcelAddin
{
    internal class CachedClient : IDisposable
    {
        private class FieldItem
        {
            internal object Value;
            internal readonly List<int> TopicIds = new List<int>();

            internal FieldItem(object value)
            {
                Value = value;
            }
        }

        private class FieldItemMap : Dictionary<string, FieldItem> { }
        private class TopicFieldItemMap : Dictionary<string, FieldItemMap> { }

        internal event Action OnUpdated;
        private readonly Client client = new Client();
        private readonly TopicFieldItemMap subscriptionCache = new TopicFieldItemMap();
        private readonly Dictionary<int, TopicFieldKey> topicCache = new Dictionary<int, TopicFieldKey>();
        private readonly List<int> updatedTopicIds = new List<int>();
        private bool isUpdated;

        internal CachedClient()
        {
            client.OnData += OnData;
        }

        internal IEnumerable<KeyValuePair<int, object>> GetUpdatedTopics()
        {
            lock (subscriptionCache)
            {
                List<KeyValuePair<int, object>> values = new List<KeyValuePair<int, object>>();

                lock (topicCache)
                {
                    lock (updatedTopicIds)
                    {
                        foreach (int topicId in updatedTopicIds)
                        {
                            TopicFieldKey topic = topicCache[topicId];
                            values.Add(
                                new KeyValuePair<int, object>(
                                    topicId,
                                    subscriptionCache[topicCache[topicId].Topic][topicCache[topicId].Field].Value));
                        }
                    }
                }

                return values;
            }
        }

        private void OnData(string topic, IDictionary<string, object> data, bool isImage)
        {
            lock (subscriptionCache)
            {
                FieldItemMap fieldItems;
                if (!subscriptionCache.TryGetValue(topic, out fieldItems))
                    return;

                if (isImage)
                {
                    // Null out any fields which are not in this messages
                    foreach (KeyValuePair<string, FieldItem> existingFieldItem in fieldItems)
                    {
                        if (data == null || !data.ContainsKey(existingFieldItem.Key))
                        {
                            existingFieldItem.Value.Value = ExcelUtils.XlErrNA;
                            SetTopicIdsUpdated(existingFieldItem.Value.TopicIds);
                        }
                    }

                    IsUpdated = true;
                }

                if (data == null)
                {
                    foreach (FieldItem fieldItem in fieldItems.Values)
                    {
                        fieldItem.Value = ExcelUtils.XlErrNA;
                        SetTopicIdsUpdated(fieldItem.TopicIds);
                    }
                }
                else
                {
                    foreach (KeyValuePair<string, object> dataItem in data)
                    {
                        FieldItem fieldItem;
                        if (fieldItems.TryGetValue(dataItem.Key, out fieldItem))
                        {
                            fieldItem.Value = dataItem.Value;
                            SetTopicIdsUpdated(fieldItem.TopicIds);
                        }
                        else
                        {
                            fieldItems.Add(dataItem.Key, new FieldItem(dataItem.Value));
                        }
                    }
                }

                if (IsUpdated && OnUpdated != null)
                    OnUpdated();
            }
        }

        private void SetTopicIdsUpdated(ICollection<int> topicIds)
        {
            lock (topicCache)
            {
                if (topicIds.Count == 0)
                    return;

                lock (updatedTopicIds)
                {
                    isUpdated = true;

                    foreach (int topicId in topicIds)
                        if (!updatedTopicIds.Contains(topicId))
                            updatedTopicIds.Add(topicId);
                }
            }
        }

        internal bool Connect(string hostName, int port)
        {
            return client.Connect(hostName, port);
        }

        internal object RegisterTopicId(int topicId, string topic, string fieldName)
        {
            lock (topicCache)
                topicCache.Add(topicId, new TopicFieldKey(topic, fieldName));

            lock (subscriptionCache)
            {
                FieldItemMap fieldItems;
                if (!subscriptionCache.TryGetValue(topic, out fieldItems))
                {
                    fieldItems = new FieldItemMap();
                    subscriptionCache.Add(topic, fieldItems);
                    client.AddSubscription(topic);
                }

                FieldItem fieldItem;
                if (!fieldItems.TryGetValue(fieldName, out fieldItem))
                {
                    fieldItem = new FieldItem(ExcelUtils.XlErrNA);
                    fieldItems.Add(fieldName, fieldItem);
                }
                fieldItem.TopicIds.Add(topicId);

                return fieldItem.Value;
            }
        }

        internal void UnregisterTopicId(int topicId)
        {
            lock (subscriptionCache)
            {
                lock (topicCache)
                {
                    lock (updatedTopicIds)
                    {
                        if (updatedTopicIds.Contains(topicId))
                            updatedTopicIds.Remove(topicId);
                    }

                    TopicFieldKey topic;
                    if (!topicCache.TryGetValue(topicId, out topic))
                        return;

                    topicCache.Remove(topicId);

                    FieldItemMap fieldItems;
                    if (!subscriptionCache.TryGetValue(topic.Topic, out fieldItems))
                        return;
                    FieldItem fieldItem;
                    if (!fieldItems.TryGetValue(topic.Field, out fieldItem))
                        return;
                    if (!fieldItem.TopicIds.Contains(topicId))
                        return;

                    fieldItem.TopicIds.Remove(topicId);
                    if (fieldItems.Values.Sum(x => x.TopicIds.Count) == 0)
                    {
                        client.RemoveSubscription(topic.Topic);
                        subscriptionCache.Remove(topic.Topic);
                    }
                }
            }
        }

        internal bool IsUpdated
        {
            get { lock (updatedTopicIds) return isUpdated; }
            set { lock (updatedTopicIds) isUpdated = value; }
        }

        internal bool HasTopic(int topicId)
        {
            lock (topicCache)
                return topicCache.ContainsKey(topicId);
        }

        public void Dispose()
        {
            lock (subscriptionCache)
            {
                foreach (string topic in subscriptionCache.Keys)
                    client.RemoveSubscription(topic);
                subscriptionCache.Clear();

                lock (topicCache)
                {
                    lock (updatedTopicIds)
                        updatedTopicIds.Clear();

                    topicCache.Clear();
                }
            }

            client.Close();
        }
    }
}
